﻿using System;
using System.IO;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Reflection;
using System.Reflection.Emit;

namespace Fuse8.DomainFramework.Common.Utilities
{
    /// <summary>
    /// 
    /// </summary>
    public static class CloneUtility
    {
        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="source"></param>
        /// <returns></returns>
        public static T NormalClone<T>(this T source)
        {
            var type = typeof(T);

            if (!type.IsSerializable)
            {
                throw new ArgumentException(string.Format("The type {0} must be serializable", type), "source");
            }

            // Don't serialize a null object, simply return the default for that object
            if (object.ReferenceEquals(source, null))
            {
                return default(T);
            }

            IFormatter formatter = new BinaryFormatter();
            using (var stream = new MemoryStream())
            {
                formatter.Serialize(stream, source);
                stream.Seek(0, SeekOrigin.Begin);

                return (T)formatter.Deserialize(stream);
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="source"></param>
        /// <returns></returns>
        public static T FastClone<T>(this T source)
        {
            return FastClone(source, false);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="source"></param>
        /// <param name="deepClone"></param>
        /// <returns></returns>
        public static T FastClone<T>(this T source, bool deepClone)
        {
            return source != null ?
                deepClone ? CloneUtilityImpl<T>.CloneDeep(source) : CloneUtilityImpl<T>.CloneShallow(source) :
                NormalClone(source);
        }
    }

    /// <summary>
    /// 
    /// </summary>
    /// <typeparam name="T"></typeparam>
    internal static class CloneUtilityImpl<T>
    {
        private static LRUCacheBag<Type, Delegate> _cachedIL = new LRUCacheBag<Type,Delegate>();
        private static LRUCacheBag<Type, Delegate> _cachedILDeep = new LRUCacheBag<Type,Delegate>();
        private static LocalBuilder _localBuilderTemp;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="source"></param>
        /// <returns></returns>
        public static T CloneShallow(T source)
        {
            Delegate myExec = null;
            Type type = typeof(T);
            if (!_cachedIL.TryGetValue(type, out myExec))
            {
                ILGenerator generator = null;

                DynamicMethod dymMethod = CreateCloneMethod(source, out generator);

                foreach (FieldInfo field in type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public))
                {
                    generator.Emit(OpCodes.Ldloc_0);
                    generator.Emit(OpCodes.Ldarg_0);
                    generator.Emit(OpCodes.Ldfld, field);
                    generator.Emit(OpCodes.Stfld, field);
                }

                generator.Emit(OpCodes.Ldloc_0);
                generator.Emit(OpCodes.Ret);

                myExec = dymMethod.CreateDelegate(typeof(Func<T, T>));

                _cachedIL.Add(typeof(T), myExec);
            }

            return (myExec as Func<T, T>)(source);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="source"></param>
        /// <returns></returns>
        public static T CloneDeep(T source)
        {
            Delegate myExec = null;
            Type type = typeof(T);

            if (!_cachedILDeep.TryGetValue(type, out myExec))
            {
                ILGenerator generator = null;

                DynamicMethod dymMethod = CreateCloneMethod(source, out generator);

                foreach (FieldInfo field in type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public))
                {
                    var fieldType = field.FieldType;                   

                    if (fieldType.IsValueType || (fieldType == typeof(string)))
                    {
                        CopyValueType(generator, field);
                    }
                    else if (fieldType.IsClass)
                    {
                        CopyReferenceType(generator, field);
                    }
                }

                generator.Emit(OpCodes.Ldloc_0);
                generator.Emit(OpCodes.Ret);

                myExec = dymMethod.CreateDelegate(typeof(Func<T, T>));

                _cachedILDeep.Add(typeof(T), myExec);
            }

            return (myExec as Func<T, T>)(source);
        }

        private static DynamicMethod CreateCloneMethod(T source, out ILGenerator generator)
        {
            var type = typeof(T);
            DynamicMethod dynamicMethod = new DynamicMethod("DoClone", type,
                    new Type[] { type }, Assembly.GetExecutingAssembly().ManifestModule, true);

            ConstructorInfo cInfo = null;
            if (type != typeof(string))
            {
                cInfo = source.GetType().GetConstructor(new Type[] { });
            }

            generator = dynamicMethod.GetILGenerator();

            LocalBuilder localBuilder = generator.DeclareLocal(typeof(T));

            if (cInfo != null)
            {
                generator.Emit(OpCodes.Newobj, cInfo);
            }
            else
            {
                generator.Emit(OpCodes.Ldstr, source.ToString());
            }

            generator.Emit(OpCodes.Stloc_0);

            return dynamicMethod;
        }

        private static void CopyValueType(ILGenerator generator, FieldInfo field)
        {
            generator.Emit(OpCodes.Ldloc_0);
            generator.Emit(OpCodes.Ldarg_0);
            generator.Emit(OpCodes.Ldfld, field);
            generator.Emit(OpCodes.Stfld, field);
        }

        private static void CopyReferenceType(ILGenerator generator, FieldInfo field)
        {
            // We have a reference type.
            _localBuilderTemp = generator.DeclareLocal(field.FieldType);
            if (field.FieldType.GetInterface("IEnumerable") != null)
            {
                // We have a list type (generic).
                if (field.FieldType.IsGenericType)
                {
                    // Get argument of list type
                    Type argType = field.FieldType.GetGenericArguments()[0];
                    // Check that it has a constructor that accepts another IEnumerable.
                    Type genericType = Type.GetType("System.Collections.Generic.IEnumerable`1["
                            + argType.FullName + "]");

                    ConstructorInfo ci = field.FieldType.GetConstructor(new Type[] { genericType });
                    if (ci != null)
                    {
                        // It has! (Like the List<> class)
                        generator.Emit(OpCodes.Ldarg_0);
                        generator.Emit(OpCodes.Ldfld, field);
                        generator.Emit(OpCodes.Newobj, ci);
                        generator.Emit(OpCodes.Stloc, _localBuilderTemp);
                        PlaceNewTempObjInClone(generator, field);
                    }
                }
            }
            else
            {
                CreateNewTempObject(generator, field.FieldType);
                PlaceNewTempObjInClone(generator, field);
                foreach (FieldInfo fi in field.FieldType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public))
                {
                    if (fi.FieldType.IsValueType || fi.FieldType == typeof(string))
                    {
                        CopyValueTypeTemp(generator, field, fi);
                    }
                    else if (fi.FieldType.IsClass)
                    {
                        CopyReferenceType(generator, fi);
                    }
                }
            }
        }

        private static void CopyValueTypeTemp(ILGenerator generator, FieldInfo fieldParent, FieldInfo fieldDetail)
        {
            generator.Emit(OpCodes.Ldloc_1);
            generator.Emit(OpCodes.Ldarg_0);
            generator.Emit(OpCodes.Ldfld, fieldParent);
            generator.Emit(OpCodes.Ldfld, fieldDetail);
            generator.Emit(OpCodes.Stfld, fieldDetail);
        }

        private static void CreateNewTempObject(ILGenerator generator, Type type)
        {
            ConstructorInfo cInfo = type.GetConstructor(new Type[] { });

            generator.Emit(OpCodes.Newobj, cInfo);
            generator.Emit(OpCodes.Stloc, _localBuilderTemp);
        }

        private static void PlaceNewTempObjInClone(ILGenerator generator, FieldInfo field)
        {
            // Get object from custom location and store it in right field of location 0
            generator.Emit(OpCodes.Ldloc_0);
            generator.Emit(OpCodes.Ldloc, _localBuilderTemp);
            generator.Emit(OpCodes.Stfld, field);
        }
    }
}
